#include <getopt.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>

#include "main.h"
#include "symtab.h"
#include "ir_code.h"

/* Constants */
static const char *C_EXT = ".c";
static const char *IR_EXT = ".ir";
static const char *OUTPUT_EXT = ".asm";
extern FILE *yyin;
extern int yyparse(void);
extern int correct;

cc_options_t cc_options = { .print_ir = 0, .ir_file = NULL, .force = 0, .dbg = 0, .silent = 0, .log = "stdout",
      .input_file = NULL, .output_file = NULL };

/** 
 * \brief Print the help.
 * \param prg_name The file name of the executable.
 */
void print_usage(const char *prg_name) {
	fprintf(stderr, "Usage: %s [-p] [-f] [-d [debug]] [-o output] [-s] [-h] file\n", prg_name);
}

/**
 * \brief Determines if the given filename has the file extension 'ext'.
 * \param file The filename to be checked.
 * \param ext The extension to check for.
 * \return A flag indicating if the filename has the specified file
 *         extension.
 *         0 = file has not the specified file extension
 *         1 = file has the specified file extension
 *
 *         It returns 0 if any of the input parameters is NULL.
 */
int has_file_extension(const char *file, const char *ext) {

	if (file == NULL || ext == NULL)
		return 0;

	size_t file_len = strlen(file);
	size_t ext_len = strlen(ext);

	if (ext_len > file_len) {
		return 0;
	} else {
		return !strncmp(file + file_len - ext_len, ext, ext_len);
	}
}

/**
 * \brief Determines the basename of the file, though without
 *        file extension.
 * \param file The file name.
 * \return The basename of the specified file. If the file name
 *         does not contain any extension, the input filename is
 *         returned. However, the string returned by this function
 *         is always a newly allocated memory block (malloc)  which 
 *         needs to be freed by the caller. 
 *
 *         It returns NULL if the input parameter is NULL or if 
 *         memory could not be allocated.
 */
char *get_file_basename(const char *file) {
	size_t baselen;
	char *filebase, *pos_ext;
	;

	if (file == NULL)
		return NULL;

	pos_ext = strrchr(file, '.');

	if (pos_ext != NULL) {
		/* The file name has a .xx extension */
		baselen = pos_ext - cc_options.input_file;

		filebase = malloc((baselen + 1) * sizeof(*filebase));
		if (filebase == NULL) {
			FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "get_file_basename -> malloc");
			return NULL;
		}
		strncpy(filebase, file, baselen);
		filebase[baselen] = '\0';
	} else {
		/* No file extensions found */
		char *tmp = strdup(file);
		if (tmp == NULL) {
			FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "get_file_basename -> strdup");
			return NULL;
		}
		return tmp;
	}

	return filebase;
}

/**
 * \brief Appends the 'ext' to the file basename 'filebase'. 
 * \param filebase The file basename.
 * \param ext The file extension.
 * \return A filename with extension 'ext'. The returned string
 *         is always a newly allocated memory block (malloc) 
 *         that needs to be freed by the caller.
 * 
 *         It returns NULL if 'filebase' is NULL or memory could not
 *         be allocated. It returns filebase if 'ext' is NULL.
 */
char *get_filename_with_ext(const char *filebase, const char *ext) {

	if (filebase == NULL)
		return NULL;
	if (ext == NULL) {
		char *tmp = strdup(filebase);
		if (tmp == NULL) {
			FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "get_filename_with_ext while calling strdup");
			return NULL;
		}
		return tmp;
	}

	size_t extlen = strlen(ext);
	size_t baselen = strlen(filebase);
	size_t strlen = baselen + extlen + 1;

	char *file = malloc(strlen * sizeof(*file));
	if (file == NULL) {
		FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "get_filename_with_ext while calling malloc");
		return NULL;
	}
	sprintf(file, "%s%s", filebase, ext);

	return file;
}

/**
 * \brief Free memory allocated in options structure.
 */
void free_options(void *data) {
	if (cc_options.ir_file != NULL)
		free(cc_options.ir_file);
	if (cc_options.input_file != NULL)
		free(cc_options.input_file);
	if (cc_options.output_file != NULL)
		free(cc_options.output_file);
}

void fprintf_head(FILE* f) {
	fprintf(f, "/****************************************************************************/\n");
	fprintf(f, " * This file was generated by the forty-two yacp compiler.                  * \n");
	fprintf(f, " * Have a look at our code repository: https://code.google.com/p/forty-two/ * \n");
	fprintf(f, " * Authors: Marcel Schroeder, Moritz Hader, Daniel Andres                   * \n");
	fprintf(f, "/****************************************************************************/\n\n");
}

/** 
 * \brief Process compiler options.
 * \param argc The number of input parameters.
 * \param argv The input parameter strings.
 * \return Indicates if processing was successful. 
 *         0 = processing successful
 *         1 = processing not successful
 *
 * Processes the input options passed to the compiler
 * and fill the compiler options structure as appropriate.
 * 
 * The following options are supported:
 *  -h: print help
 *  -p: print the IR to a file
 *  -f: IR file overwrites a existing one with same name
 *  -o: the output file name (different from 'input'.o)
 */
int process_options(int argc, char *argv[]) {
	int opt;
	int ret = 0;

	/* add a handler to resource manager to free resources
	 * in the case of an error during option processing */
	rm_register_handler(&resource_mgr, free_options, NULL);

	while ((opt = getopt(argc, argv, "hpfd::o:s")) != -1) {
		switch (opt) {
			case 'p':
				cc_options.print_ir = 1;
				break;
			case 'f':
				cc_options.force = 1;
				break;
			case 'd':
				/* debug messages */
				cc_options.dbg = 1;
				if (optarg != NULL) {
					cc_options.log = strdup(optarg);
					if (cc_options.log == NULL) {
						FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "process_options while calling strdup");
						return 1;
					}
				}
				break;
			case 'o':
				/* output file */
				cc_options.output_file = strdup(optarg);
				if (cc_options.output_file == NULL) {
					FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "process_options while calling strdup");
					return 1;
				}
				break;
			case 's':
				/* silent flag */
				cc_options.silent = 1;
				break;
			case 'h':
				/* print help */
				print_usage(argv[0]);
				rm_cleanup_resources(&resource_mgr);
				exit(EXIT_SUCCESS);
			default: /* '?' */
				/* print usage */
				fprintf(stderr, "ERROR: unkown parameter: %s\n", argv[optind]);
				print_usage(argv[0]);
				return 1;
		}
	}

	if (optind >= argc) {
		fprintf(stderr, "ERROR: missing input file\n");
		print_usage(argv[0]);
		ret = 1;
	} else if (optind < argc - 1) {
		fprintf(stderr, "ERROR: too many input files\n");
		print_usage(argv[0]);
		ret = 1;
	} else {
		cc_options.input_file = strdup(argv[optind]);
		if (cc_options.input_file == NULL) {
			FATAL_OS_ERROR(OUT_OF_MEMORY, 0, "process_options while calling strdup");
			return 1;
		}

		char *filebase = get_file_basename(cc_options.input_file);
		;
		if (filebase == NULL) {
			return 1;
		}

		if (!has_file_extension(cc_options.input_file, C_EXT)) {
			fprintf(stderr, "ERROR: no C file (.c) as input\n");
			ret = 1;
		} else {
			/* The file name has a valid .c extension */
			if (cc_options.output_file == NULL) {
				/* create output file name <input>.o */
				cc_options.output_file = get_filename_with_ext(filebase, OUTPUT_EXT);
				if (cc_options.output_file == NULL) {
					ret = 1;
				}
			}
			if (cc_options.print_ir == 1) {
				/* create IR file name <input>.ir */
				cc_options.ir_file = get_filename_with_ext(filebase, IR_EXT);
				if (cc_options.ir_file == NULL) {
					ret = 1;
				} else {
					// Create a new file if not exists. When force is set to 0 check if file already exists and fail it does.
					if (cc_options.force == 0) {
						FILE* f = fopen(cc_options.ir_file, "wx");
						if (!f) {
							fprintf(stderr, "ERROR: IR File could not be created. Because it already exists. \n");
							ret = 1;
						} else {
							fprintf_head(f);
							fclose(f);
						}
					} else { //creates a file without checking if exists... FORCE mode
						FILE* f = fopen(cc_options.ir_file, "w");
						if (!f) {
							fprintf(stderr, "ERROR: IR File could not be created. \n");
							ret = 1;
						} else {
							fprintf_head(f);
							fclose(f);
						}
					}
				}
			}
		}

		// initialize debug logger file
		// Create a new file if not exists. When force is set to 0 check if file already exists and fail it does.
		if (cc_options.log != NULL) {
			if ((strcmp(cc_options.log, "stdout") == 0) || (strcmp(cc_options.log, "stderr") == 0)) {

			} else if (cc_options.force == 0) {
				FILE* f = fopen(cc_options.log, "wx");
				if (!f) {
					fprintf(stderr, "ERROR: Debug Log File could not be created. Because it already exists. \n");
					ret = 1;
				} else {
					fprintf_head(f);
					fclose(f);
				}
			} else { //creates a file without checking if exists... FORCE mode
				FILE* f = fopen(cc_options.log, "w");
				if (!f) {
					fprintf(stderr, "ERROR: Debug Log File could not be created. \n");
					ret = 1;
				} else {
					fprintf_head(f);
					fclose(f);
				}
			}
		}
		free(filebase);
	}
	return ret;
}

void debug(char *string, ...) {
	if (cc_options.dbg == 1 && cc_options.silent == 0) {
		FILE* f;
		if (strcmp(cc_options.log, "stdout") == 0) {
			f = stdout;
		} else if (strcmp(cc_options.log, "stderr") == 0) {
			f = stderr;
		} else
			f = fopen(cc_options.log, "a+");
		va_list ap;
		va_start(ap, string);
		vfprintf(f, string, ap);
		fprintf(f, "\n");
		if (!strcmp(cc_options.log, "stdout") && !strcmp(cc_options.log, "stderr"))
			fclose(f);
	}
}
void info(char *string, ...) {
	if (cc_options.silent == 0) {
		FILE* f;
		if (strcmp(cc_options.log, "stdout") == 0) {
			f = stdout;
		} else if (strcmp(cc_options.log, "stderr") == 0) {
			f = stderr;
		} else
			f = fopen(cc_options.log, "a+");
		va_list ap;
		va_start(ap, string);
		vfprintf(f, string, ap);
		fprintf(f, "\n");
		if (!strcmp(cc_options.log, "stdout") && !strcmp(cc_options.log, "stderr"))
			fclose(f);
	}
}
void warning(char *string, ...) {
	if (cc_options.silent == 0) {
		FILE* f;
		if (strcmp(cc_options.log, "stdout") == 0) {
			f = stdout;
		} else if (strcmp(cc_options.log, "stderr") == 0) {
			f = stderr;
		} else
			f = fopen(cc_options.log, "a+");
		va_list ap;
		va_start(ap, string);
		fprintf(f, "WARNING: ");
		vfprintf(f, string, ap);
		fprintf(f, "\n");
		if (!strcmp(cc_options.log, "stdout") && !strcmp(cc_options.log, "stderr"))
			fclose(f);
	}
}

/**
 * \brief Entry point.
 * \param argc The number of input parameters.
 * \param argv The input parameters.
 */
int main(int argc, char *argv[]) {
	int parser_return = 1; // parser return value; initialisiert auf 1, sodass nur ein korrekter durchlauf (0) auch zum erfolg führt
	/* the resource manager must be initialized before any
	 * further actions are implemented */
	rm_init(&resource_mgr);

	if (process_options(argc, argv) == 1) {
		info("****** Compiling failed. ******");
		rm_cleanup_resources(&resource_mgr);
		exit(EXIT_FAILURE);
	}

	info("Input: %s", cc_options.input_file);
	info("Output: %s", cc_options.output_file);
	if (cc_options.print_ir == 1)
		info("IR: %s", cc_options.ir_file);
	if (cc_options.log != NULL)
		info("Debug Log: %s", cc_options.log);

	info("");
	info("****** Compiling started. ******");

	yyin = fopen(cc_options.input_file, "r");
	if (!yyin) {
		fprintf(stderr, "couldn't open input file for reading\n");
		exit(1);
	}
	parser_return = yyparse();
	fclose(yyin);

	if (parser_return == 0 && correct == 1) {
		if (cc_options.print_ir == 1) {
			printSymTable(cc_options.ir_file);
			printIrCode(cc_options.ir_file);
		}
		info("****** Compiling ended successfull. ******");
	} else {
		info("****** Compiling failed. ******");
	}
	//system("rm Symtable.txt");
	//printSymTable("Symtable.txt");
	//system("rm IRCode.txt");
	//printIrCode("IRCode.txt");

	//everythingEnds(NULL, 0); // auskommentiert, da das Programm oft nicht terminiert
	free_ir();
	rm_cleanup_resources(&resource_mgr);
	info("Resources cleaned up.");
	return 0;
}

